/*
 * Copyright 2006-2008 Michele Mostarda ( michele.mostarda@gmail.com ).
 * All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.graphnav.model;

import org.graphnav.model.event.GraphModelEvent;
import org.graphnav.model.event.GraphModelListener;
import org.graphnav.swing.Arc;
import org.graphnav.swing.Node;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * TODO: optimize the EMPTY_FILTER usage...
 *
 * Extends a <code>DefaultGraphModel</code> to support filters,
 * providing a decoration pattern on a <code>GraphModel</code>.
 *
 * @see org.graphnav.model.GraphModel
 */
public class FilteredGraphModel extends DefaultGraphModel {

    /**
     * This internal listener is used by the <code>FilteredGraphModel</code>
     * to be notified wether the underlying model changes.
     */
    private class InternalListener implements GraphModelListener {

        public void modelChanged(GraphModelEvent gme) {
            validate();
            notifyModelChanged();
        }
    }

    /**
     * Defines an empty filter that doesn't filter anything.
     */
    public static final Filter EMPTY_FILTER = new Filter() {

        public void init() {}

        public void release() {}

        public boolean nodeIsVisible(GraphModel model, Node node) {
            throw new UnsupportedOperationException();
            //return true;
        }

        public boolean arcIsVisible(GraphModel model, Arc node) {
            throw new UnsupportedOperationException();
            //return true;
        }
    };

    /**
     * Defines the status ready for the filter.
     */
    private final static int READY     = 0;

    /**
     * Defines the status visiting (processing) for the filter.
     */
    private final static int VISITING  = 1;

    /**
     * The <code>InternalListener</code> instance.
     */
    private final InternalListener internalListener;

    /**
     * The decorated <code>GraphModel</code>.
     */
    private GraphModel decorated;

    /**
     * Empty filter instance.
     */
    private Filter filter = EMPTY_FILTER;

    /**
     * Set of filtered nodes.
     */
    private Set nodesSet;

    /**
     * Mantains the status of the filter.
     */
    private int status = READY;

    /**
     * Constructor.
     *
     * @param decorated the decorated model
     * @param filter the filter to be applied
     */
    public FilteredGraphModel(GraphModel decorated, Filter filter) {
        internalListener = new InternalListener();
        nodesSet = new HashSet();
        setDecoratedModel(decorated);
        setFilter(filter);
    }

    /**
     * Changes the decorated model whith the given, <b>forcing</b> a reprocessing od the model.
     *
     * @param gm the new decorated model.
     */
    public void setDecoratedModel(GraphModel gm) {
        if(gm == null) {
            throw new NullPointerException();
        }
        if(decorated != null) {
            decorated.removeGraphModelListener(internalListener);
        }
        gm.addGraphModelListener(internalListener);
        decorated = gm;

        validate();
        notifyModelChanged();
    }

    /**
     * Returns the decorated model.
     * 
     * @return
     */
    public GraphModel getDecoratedModel() {
        return decorated;
    }

    /**
     * Sets the filter to be applied on the decorated model.
     * Setting a new filter will <b>force</b> a new re evaluation of the model. 
     *
     * @param f
     * @return
     */
    public Excluded setFilter(Filter f) {
        if(f == null) {
            throw new NullPointerException();
        }
        filter = f;

        Excluded excluded = validate();
        notifyModelChanged();
        return excluded;
    }

    /**
     * Processes the given filter on the wrapped model whitout change the FilteredGraphModel status.
     *
     * @param f the filter to apply.
     * @return  an object representing the excluded objects.
     */
    public Excluded simulateFilter(Filter f) {
        if(f == null) {
            throw new NullPointerException();
        }

        Excluded excluded = computeFiltering(f, new ArrayList(), new ArrayList());
        return excluded;
    }

    /**
     * Returns the current filter.
     *
     * @return
     */
    public Filter getFilter() {
        return filter;
    }

    /**
     * Force the evaluation of the filter on the wrapper model to update the FilteredGraphModel.
     *
     * @return
     */
    public Excluded validate() {
        Excluded exc = computeFiltering(filter, nodes, arcs);
        notifyModelChanged();
        return  exc;
    }

    /**
     * Applies a filter on a list of nodes and a list of related arcs.
     *
     * @param ftr
     * @param nds
     * @param acs
     * @return
     */
    protected Excluded computeFiltering(Filter ftr, List nds, List acs) {
        if(ftr == EMPTY_FILTER) {
            return Excluded.EMPTY;
        }

        nds.clear();
        acs.clear();

        ftr.init();
        Iterator nodesIterator = decorated.nodes();
        Node node;
        List excludedNodes = new ArrayList();
        while(nodesIterator.hasNext()) {
            node = (Node) nodesIterator.next();
            if( ftr.nodeIsVisible(decorated, node) ) {
                nodesSet.add(node);
            } else {
                excludedNodes.add(node);
            }
        }
        Iterator arcsIterator = decorated.arcs();
        List excludedArcs  = new ArrayList();
        Arc arc;
        while(arcsIterator.hasNext()) {
            arc = (Arc) arcsIterator.next();
            if(
                    nodesSet.contains( arc.getFromNode() )
                    &&
                    nodesSet.contains( arc.getToNode()   )
                    &&
                    ftr.arcIsVisible(decorated, arc)
            ) {
                acs.add(arc);
            } else {
                excludedArcs.add(arc);
            }
        }
        nds.addAll(nodesSet);
        nodesSet.clear();
        ftr.release();

        //notifyModelChanged();
        
        return new Excluded(excludedNodes, excludedArcs);
    }

    public void startVisit() {
        if(status == READY) {
            status = VISITING;
        } else if(status == VISITING) {
            throw new IllegalStateException();
        }
    }

    public void endVisit() {
        status = READY;
    }

    public Iterator<Node> nodes() {
        if(filter == EMPTY_FILTER) {
            return decorated.nodes();
        }
        return super.nodes();
    }

    public Iterator<Arc> arcs() {
        if(filter == EMPTY_FILTER) {
            return decorated.arcs();
        }
        return super.arcs();
    }

    public Node getNode(int i) {
        if(filter == EMPTY_FILTER) {
            return decorated.getNode(i);
        }
        return super.getNode(i);
    }

    public Arc getArc(int i) {
        if(filter == EMPTY_FILTER) {
            return decorated.getArc(i);
        }
        return super.getArc(i);
    }

    public int nodesCount() {
        if(filter == EMPTY_FILTER) {
            return decorated.nodesCount();
        }
        return super.nodesCount();
    }

    public int arcsCount() {
        if(filter == EMPTY_FILTER) {
            return decorated.arcsCount();
        }
        return super.arcsCount();
    }

    public void removeNode(Node node) {
       if(decorated instanceof ModifiableGraphModel) {
           ( (ModifiableGraphModel) decorated).removeNode(node);
       } else {
           throw new UnsupportedOperationException();
       }
    }

    public void removeArc(Arc a) {
       if(decorated instanceof ModifiableGraphModel) {
           ( (ModifiableGraphModel) decorated).removeArc(a);
       } else {
           throw new UnsupportedOperationException();
       }
    }

}
